/* pi_main.c is small program using the asciiboard API, which is
   designed to run in realtime on a Raspberry Pi running
   a linux distro.  The program is designed to interface with
   an adapted keyboard and a gyroscope module using UART.

   James Boyle (jab1g12@soton.ac.uk) copyright (c) 2014 */
#include <stdio.h>
#include "rpi/pi_main.h"
#include "asciiboard.h"
#include "asciiboard_const.h"
#include "rpi/ascii_gyro.h"
/* uart headers */
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
/* portaudio headers */
#include <portaudio.h>
#include <pa_linux_alsa.h>
#include "rpi/pa_ringbuffer.h"

static void diagnostic_pa(void);
static void pa_error(const int err);
static int cleanup(const int uart_filestream, const int pa_init);

float out_buffer[PERIOD_SAMPLES];
PaUtilRingBuffer sample_buff;
float buffer_data[RING_BUFFER_SIZE];

/* pa_callback() is passed to port audio, and simply transfers audio from the 
  large circular buffer to the output stream.  Samples are read regardless
  of whether the circular buffer contains enough samples - the result will
  be audio dropouts regardless. */
static int pa_callback(const void *inputBuffer, void *outputBuffer,
		               unsigned long framesPerBuffer,
                       const PaStreamCallbackTimeInfo* timeInfo,
                       PaStreamCallbackFlags statusFlags,
                       void *user_data )
{
	/* cast data passed to function */
	float *out = (float*) outputBuffer;
	(void) inputBuffer; /* prevent unused variable warning. */

	PaUtil_ReadRingBuffer(&sample_buff, (void *) out, framesPerBuffer);
	return paContinue;
}

static void process(const float *buffer, const int size, void *args)
{
	int i;
	float amplified[PERIOD_SAMPLES];
	/* apply a small gain as the output can be quiet
	   when only one note is pressed, TODO: use a
	   dynamic compressor to more effectively
	   amplify */
	for (i = 0; i < size; i++)
		amplified[i] = buffer[i];
	amplify(amplified, PI_GAIN, size);

	while (PaUtil_GetRingBufferWriteAvailable(&sample_buff) < size);
	PaUtil_WriteRingBuffer(&sample_buff, (void *) amplified, size);
}

static int pi_timeout(void)
{
	if (PaUtil_GetRingBufferReadAvailable(&sample_buff) < PA_BUFFER_SIZE)
		return TRUE;
	else
		return FALSE;
}

int main(int argc, char *argv[])
{
	int i, stopped;
	char rx;
	/* uart data */
	int rx_read, uart_filestream;
	struct termios options;
	/* portaudio data */
	PaStream *stream;
	PaError err;
	float tmp[RING_BUFFER_SIZE];
	float asciiboard_out[PERIOD_SAMPLES];

	/* initialise the ring buffer, which feeds audio into the portaudio
	   callback function */
	PaUtil_InitializeRingBuffer(&sample_buff, sizeof(float),
	                            RING_BUFFER_SIZE, (void *) buffer_data);
	for (i = 0; i < RING_BUFFER_SIZE; i++)
		tmp[i] = 0.0f;
	PaUtil_WriteRingBuffer(&sample_buff, (void *) tmp, RING_BUFFER_SIZE);

	/* initialise ASCIIboard engine, note the buffer is uninitialised */
	/* TODO - saving currently not implemented */
	asciiboard_init(NULL, asciiboard_out, PERIOD_SAMPLES, process, NULL, pi_timeout);

	/* initialise the UART connection */
	uart_filestream = INVALID_STREAM;
	uart_filestream = open("/dev/ttyAMA0",
	                       O_RDWR | O_NOCTTY | O_NONBLOCK);
	if (uart_filestream == INVALID_STREAM)
	{
		/* can't open serial port */
		printf("Note: Can't open serial port.\n");
		cleanup(uart_filestream, FALSE);
		return 0;
	}

	tcgetattr(uart_filestream, &options);
	options.c_cflag = B9600 | CS8 | CLOCAL | CREAD;
	options.c_iflag = IGNPAR;
	options.c_oflag = 0;
	options.c_lflag = 0;
	tcflush(uart_filestream, TCIFLUSH);
	tcsetattr(uart_filestream, TCSANOW, &options);


	/*  initialise portaudio */
	err = Pa_Initialize();
	printf("Note: PortAudio initialised.\n");
	fflush(stdout);
	if (err != paNoError)
	{
		pa_error(err);
		cleanup(uart_filestream, TRUE);
		return 1;
	}

	err = Pa_OpenDefaultStream(&stream,
	                           0, /* no input channels */
	                           1, /* mono output */
	                           paFloat32, /* 32 bit floating point output */
	                           SRATE,
	                           PA_BUFFER_SIZE, /* frames per buffer */
	                           pa_callback, /* this is your callback function */
	                           NULL); /* custom stuff passed to callback */
	if (err != paNoError)
	{
		pa_error(err);
		cleanup(uart_filestream, TRUE);
		return 1;
	}

    /* demonstrate the capability of the engine */
	asciiboard_add_processing_func(ascii_through, CRITICAL);
	asciiboard_add_processing_func(gyro_synth, HIGH);
	asciiboard_add_processing_func(ascii_common_waveform, MED);
	asciiboard_add_processing_func(ascii_vibrato, LOW);
	asciiboard_add_processing_func(ascii_tremolo, LOW);
	/* enable real time scheduling for ALSA, which gives the thread a
		high priority suitable for real time applications */;
	PaAlsa_EnableRealtimeScheduling(stream, 1);
	/* start the stream */
	err = Pa_StartStream(stream);
	if (err != paNoError)
	{
		pa_error(err);
		return 1;
	}
	printf("Note: PortAudio stream started.\n");
	stopped = FALSE;
	while (!stopped)
	{
		/* always try and keep the circular buffer full, whenever some samples
		   are read generate another period of audio. */
		if (!streaming_data() &&
			PaUtil_GetRingBufferWriteAvailable(&sample_buff) > 0)
			asciiboard(NEXT_CODE);
		else
		{
			/* read in the next character from the serial port */
			rx_read = read(uart_filestream, (void *) &rx, 1);

			if (rx_read > 0)
			{
				printf("Character Received: ch = %c (%d)\n", rx, (int) rx);
				/* interpret a few pi specific codes, allowing the user to
				   kill the process and reset using the keyboard */
				if (rx == KILL_CODE)
					stopped = TRUE;
				else if (rx == PI_RESET_CODE)
					rx = RESET_CODE;
				asciiboard(rx);
			}
		}
	}

	/* stop the portaudio streams and close them */
	/* stop the stream */
	err = Pa_StopStream(stream);
	if (err != paNoError)
	{
		pa_error(err);
		cleanup(uart_filestream, TRUE);
		return 1;
	}
	printf("Note: PortAudio stream stopped.\n");

	/* close the stream */
	err = Pa_CloseStream(stream);
	if (err != paNoError)
	{
		pa_error(err);
		cleanup(uart_filestream, TRUE);
		return 1;
	}

	cleanup(uart_filestream, TRUE);
	/* clean up, if this fails return an error */
	if (err != paNoError)
	{
		pa_error(err);
		return 1;
	}
	else
		return 0;
}

int cleanup(const int uart_filestream, const int pa_init)
{
	int err;

	/* stop the ASCIIboard engine */
	asciiboard_finish();
	/* close the uart connection if it was set up */
	if (uart_filestream != -1)
        close(uart_filestream);
	/* if portaudio was initialised, terminate it */
	if (pa_init)
	{
		/* if portaudio can't terminate return an error */
		err = Pa_Terminate();
		if (err != paNoError)
		{
			pa_error(err);
			return 1;
		}
	}

	return 0;
}

/* pa_error() outputs a port audio error to the console. */
void pa_error(const int err)
{
	printf("PortAudio error: %s\n", Pa_GetErrorText(err));
}

/* diagnostic_pa() tests to see default latency values for port audio on the 
   pi. */
static void diagnostic_pa(void)
{
	const PaDeviceInfo *info = Pa_GetDeviceInfo((PaDeviceIndex) 0);
	printf("DIAGNOSTIC: default low output latency = %f.\n",
	       info->defaultLowOutputLatency);
	printf("DIAGNOSTIC: default high output latency = %f.\n",
	       info->defaultHighOutputLatency);
	printf("DIAGNOSTIC: default sampling rate = %f.\n",
	       info->defaultSampleRate);
}
